package fec

import (
	"github.com/lucas-clemente/quic-go/internal/protocol"
	"log"
	"github.com/lucas-clemente/quic-go/internal/utils"
)

// The redundancy control will adapt the number of FEC Repair Symbols and
// the size of the FEC Group to the current conditions.

// Warning: it assumes that the packets are sent with packet numbers increasing by one.

const MAX_LOSS_BURST_LENGTH uint = 40
const SAMPLE_SIZE								 = 500

type RedundancyController interface {
	// is called whenever a packet is lost
	OnPacketLost(protocol.PacketNumber)
	// is called whenever a packet is received
	OnPacketReceived(protocol.PacketNumber)
	// returns the number of data symbols that should compose a single FEC Group
	GetNumberOfDataSymbols() uint
	// returns the maximum number of repair symbols that should be generated for a single FEC Group
	GetNumberOfRepairSymbols() uint
}

type averageRedundancyController struct {
	initialPacketOfThisSample     protocol.PacketNumber
	packetsCounter								uint
	numberOfContiguousLostPackets	uint
	lastLostPacketNumber					protocol.PacketNumber
	burstCounter									map[uint]uint	// TODO: evaluate performances vs array [MAX_LOSS_BURST_LENGTH]uint
	interLossDistanceCounter  		map[uint]uint	// TODO: evaluate performances vs array [SAMPLE_SIZE]uint

	meanBurstLength								uint
	meanInterLossDistance					uint
}

var _ RedundancyController = &averageRedundancyController{}

func NewAverageRedundancyController() RedundancyController {
	return &averageRedundancyController{
		burstCounter:          			make(map[uint]uint),
		interLossDistanceCounter:   make(map[uint]uint),
		meanBurstLength: 			 			uint(protocol.NumberOfRepairSymbols),
		meanInterLossDistance: 			uint(protocol.NumberOfFecPackets),
	}
}

func (c *averageRedundancyController) OnPacketLost(pn protocol.PacketNumber) {
	log.Printf("LOST %d", pn)
	if c.packetsCounter == 0 {
		c.initialPacketOfThisSample = pn
	}
	if pn < c.initialPacketOfThisSample {
		return
	}
	if c.lastLostPacketNumber == pn-1 {
		// we continue the current burst
		c.numberOfContiguousLostPackets++
	} else {
		// we begin a new burst
		c.burstCounter[c.numberOfContiguousLostPackets]++
		c.interLossDistanceCounter[uint(pn-c.lastLostPacketNumber)]++
		c.numberOfContiguousLostPackets = 1
	}
	c.lastLostPacketNumber = pn
	c.incrementCounter()
}

func (c *averageRedundancyController) OnPacketReceived(pn protocol.PacketNumber) {
	if pn < c.initialPacketOfThisSample {
		return
	}
	if c.packetsCounter == 0 {
		c.initialPacketOfThisSample = pn
	}
	c.incrementCounter()
}

func (c *averageRedundancyController) GetNumberOfDataSymbols() uint {
	return c.meanInterLossDistance
}

func (c *averageRedundancyController) GetNumberOfRepairSymbols() uint {
	return c.meanBurstLength
}


func (c *averageRedundancyController) incrementCounter() {
	c.packetsCounter++
	if c.packetsCounter == SAMPLE_SIZE {
		c.computeEstimations()
		c.packetsCounter = 0
		c.burstCounter = make(map[uint]uint)
		c.interLossDistanceCounter = make(map[uint]uint)
	}
}

func (c *averageRedundancyController) computeEstimations(){
	var sumOccurrencesTimesBurstLength uint = 0
	var sumOccurrences uint = 0
	for burstLength, count := range c.burstCounter {
		sumOccurrencesTimesBurstLength += burstLength*count
		sumOccurrences += count
	}
	if sumOccurrences > 0 {
		c.meanBurstLength = movingAverage(c.meanBurstLength, sumOccurrencesTimesBurstLength/sumOccurrences, 0.5)
	} else {
		c.meanBurstLength = movingAverage(c.meanBurstLength, 0, 0.5)
	}
	var sumOccurencesTimesILD uint = 0
	sumOccurrences = 0
	for ild, count := range c.interLossDistanceCounter {
		sumOccurencesTimesILD += ild*count
		sumOccurrences += count
	}
	if sumOccurrences > 0 {
		c.meanInterLossDistance = uint(utils.MinUint32(uint32(movingAverage(c.meanInterLossDistance, sumOccurencesTimesILD/sumOccurrences, 0.7)), protocol.NumberOfFecPackets))
	} else {
		c.meanInterLossDistance = movingAverage(c.meanInterLossDistance, uint(protocol.NumberOfFecPackets), 0.7)
	}
}

func movingAverage(old uint, new uint, factor float64) uint {
	return uint((factor*float64(old)+(1-factor)*float64(new)))
}